/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.curator.test;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Abstracts one of the servers in the ensemble
 */
public class InstanceSpec {
    private static final AtomicInteger nextServerId = new AtomicInteger(1);
    private static final String localhost;

    static {
        String address = "localhost";
        try {
            // This is a workaround for people using OS X Lion.  On Lion when a process tries to connect to a link-local
            // address it takes 5 seconds to establish the connection for some reason.  So instead of using 'localhost'
            // which could return the link-local address randomly, we'll manually resolve it and look for an address to
            // return that isn't link-local.  If for some reason we can't find an address that isn't link-local then
            // we'll fall back to the default lof just looking up 'localhost'.
            for (InetAddress a : InetAddress.getAllByName("localhost")) {
                if (!a.isLinkLocalAddress()) {
                    address = a.getHostAddress();
                    break;
                }
            }
        } catch (UnknownHostException e) {
            // Something went wrong, just default to the existing approach of using 'localhost'.
        }
        localhost = address;
    }

    private final File dataDirectory;
    private final int port;
    private final int electionPort;
    private final int quorumPort;
    private final boolean deleteDataDirectoryOnClose;
    private final int serverId;
    private final int tickTime;
    private final int maxClientCnxns;
    private final Map<String, Object> customProperties;
    private final String hostname;

    public static void reset() {
        nextServerId.set(1);
    }

    /**
     * Create a builder to construct {@link InstanceSpec}.
     */
    public static InstanceSpecBuilder builder() {
        return new InstanceSpecBuilder();
    }

    public static InstanceSpec newInstanceSpec() {
        return builder().build();
    }

    public static int getRandomPort() {
        ServerSocket server = null;
        try {
            server = new ServerSocket(0);
            server.setReuseAddress(true);
            return server.getLocalPort();
        } catch (IOException e) {
            throw new Error(e);
        } finally {
            if (server != null) {
                try {
                    server.close();
                } catch (IOException ignore) {
                    // ignore
                }
            }
        }
    }

    /**
     * @deprecated Use {@link #builder()} instead.
     *
     * @param dataDirectory              where to store data/logs/etc.
     * @param port                       the port to listen on - each server in the ensemble must use a unique port
     * @param electionPort               the electionPort to listen on - each server in the ensemble must use a unique electionPort
     * @param quorumPort                 the quorumPort to listen on - each server in the ensemble must use a unique quorumPort
     * @param deleteDataDirectoryOnClose if true, the data directory will be deleted when {@link TestingCluster#close()} is called
     * @param serverId                   the server ID for the instance
     */
    @Deprecated
    public InstanceSpec(
            File dataDirectory,
            int port,
            int electionPort,
            int quorumPort,
            boolean deleteDataDirectoryOnClose,
            int serverId) {
        this(dataDirectory, port, electionPort, quorumPort, deleteDataDirectoryOnClose, serverId, -1, -1, null, null);
    }

    /**
     * @deprecated Use {@link #builder()} instead.
     *
     * @param dataDirectory              where to store data/logs/etc.
     * @param port                       the port to listen on - each server in the ensemble must use a unique port
     * @param electionPort               the electionPort to listen on - each server in the ensemble must use a unique electionPort
     * @param quorumPort                 the quorumPort to listen on - each server in the ensemble must use a unique quorumPort
     * @param deleteDataDirectoryOnClose if true, the data directory will be deleted when {@link TestingCluster#close()} is called
     * @param serverId                   the server ID for the instance
     * @param tickTime                   tickTime. Set -1 to used fault server configuration
     * @param maxClientCnxns             max number of client connections from the same IP. Set -1 to use default server configuration
     */
    @Deprecated
    public InstanceSpec(
            File dataDirectory,
            int port,
            int electionPort,
            int quorumPort,
            boolean deleteDataDirectoryOnClose,
            int serverId,
            int tickTime,
            int maxClientCnxns) {
        this(
                dataDirectory,
                port,
                electionPort,
                quorumPort,
                deleteDataDirectoryOnClose,
                serverId,
                tickTime,
                maxClientCnxns,
                null,
                null);
    }

    /**
     * @deprecated Use {@link #builder()} instead.
     *
     * @param dataDirectory              where to store data/logs/etc.
     * @param port                       the port to listen on - each server in the ensemble must use a unique port
     * @param electionPort               the electionPort to listen on - each server in the ensemble must use a unique electionPort
     * @param quorumPort                 the quorumPort to listen on - each server in the ensemble must use a unique quorumPort
     * @param deleteDataDirectoryOnClose if true, the data directory will be deleted when {@link TestingCluster#close()} is called
     * @param serverId                   the server ID for the instance
     * @param tickTime                   tickTime. Set -1 to used fault server configuration
     * @param maxClientCnxns             max number of client connections from the same IP. Set -1 to use default server configuration
     * @param customProperties           other properties to be passed to the server
     */
    @Deprecated
    public InstanceSpec(
            File dataDirectory,
            int port,
            int electionPort,
            int quorumPort,
            boolean deleteDataDirectoryOnClose,
            int serverId,
            int tickTime,
            int maxClientCnxns,
            Map<String, Object> customProperties) {
        this(
                dataDirectory,
                port,
                electionPort,
                quorumPort,
                deleteDataDirectoryOnClose,
                serverId,
                tickTime,
                maxClientCnxns,
                customProperties,
                null);
    }

    /**
     * @deprecated Use {@link #builder()} instead.
     *
     * @param dataDirectory              where to store data/logs/etc.
     * @param port                       the port to listen on - each server in the ensemble must use a unique port
     * @param electionPort               the electionPort to listen on - each server in the ensemble must use a unique electionPort
     * @param quorumPort                 the quorumPort to listen on - each server in the ensemble must use a unique quorumPort
     * @param deleteDataDirectoryOnClose if true, the data directory will be deleted when {@link TestingCluster#close()} is called
     * @param serverId                   the server ID for the instance
     * @param tickTime                   tickTime. Set -1 to used fault server configuration
     * @param maxClientCnxns             max number of client connections from the same IP. Set -1 to use default server configuration
     * @param customProperties           other properties to be passed to the server
     * @param hostname                   Hostname or IP if the cluster is intending to be bounded into external interfaces
     */
    @Deprecated
    public InstanceSpec(
            File dataDirectory,
            int port,
            int electionPort,
            int quorumPort,
            boolean deleteDataDirectoryOnClose,
            int serverId,
            int tickTime,
            int maxClientCnxns,
            Map<String, Object> customProperties,
            String hostname) {
        this(
                dataDirectory,
                port,
                electionPort,
                quorumPort,
                deleteDataDirectoryOnClose,
                serverId,
                tickTime,
                maxClientCnxns,
                customProperties != null ? enforceStringMap(customProperties) : null,
                hostname,
                false);
    }

    InstanceSpec(
            File dataDirectory,
            int port,
            int electionPort,
            int quorumPort,
            boolean deleteDataDirectoryOnClose,
            int serverId,
            int tickTime,
            int maxClientCnxns,
            Map<String, Object> customProperties,
            String hostname,
            boolean ignored) {
        this.dataDirectory = (dataDirectory != null) ? dataDirectory : DirectoryUtils.createTempDirectory();
        this.port = (port >= 0) ? port : getRandomPort();
        this.electionPort = (electionPort >= 0) ? electionPort : getRandomPort();
        this.quorumPort = (quorumPort >= 0) ? quorumPort : getRandomPort();
        this.deleteDataDirectoryOnClose = deleteDataDirectoryOnClose;
        this.serverId = (serverId >= 0) ? serverId : nextServerId.getAndIncrement();
        this.tickTime = (tickTime > 0 ? tickTime : -1); // -1 to set default value
        this.maxClientCnxns = (maxClientCnxns >= 0 ? maxClientCnxns : -1); // -1 to set default value
        this.customProperties =
                customProperties != null ? Collections.unmodifiableMap(customProperties) : Collections.emptyMap();
        this.hostname = hostname == null ? localhost : hostname;
    }

    private static Map<String, Object> enforceStringMap(Map<String, Object> properties) {
        for (Map.Entry<String, Object> entry : properties.entrySet()) {
            if (!(entry.getValue() instanceof String)) {
                String msg = String.format("property %s has non string value %s", entry.getKey(), entry.getValue());
                throw new IllegalArgumentException(msg);
            }
        }
        return properties;
    }

    public int getServerId() {
        return serverId;
    }

    public File getDataDirectory() {
        return dataDirectory;
    }

    public int getPort() {
        return port;
    }

    public int getElectionPort() {
        return electionPort;
    }

    public int getQuorumPort() {
        return quorumPort;
    }

    /**
     * @deprecated use {@link TestingServer#getConnectString()} or {@link TestingCluster#getConnectString()} instead
     */
    @Deprecated
    public String getConnectString() {
        return hostname + ":" + port;
    }

    public int getTickTime() {
        return tickTime;
    }

    public int getMaxClientCnxns() {
        return maxClientCnxns;
    }

    public boolean deleteDataDirectoryOnClose() {
        return deleteDataDirectoryOnClose;
    }

    public Map<String, Object> getCustomProperties() {
        return customProperties;
    }

    public String getHostname() {
        return hostname;
    }

    @Override
    public String toString() {
        return "InstanceSpec{" + "dataDirectory="
                + dataDirectory + ", port="
                + port + ", electionPort="
                + electionPort + ", quorumPort="
                + quorumPort + ", deleteDataDirectoryOnClose="
                + deleteDataDirectoryOnClose + ", serverId="
                + serverId + ", tickTime="
                + tickTime + ", maxClientCnxns="
                + maxClientCnxns + ", customProperties="
                + customProperties + ", hostname="
                + hostname + "} "
                + super.toString();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        InstanceSpec that = (InstanceSpec) o;

        return hostname.equals(that.getHostname()) && port == that.port;
    }

    @Override
    public int hashCode() {
        return hostname.hashCode() + port;
    }
}
